import logging
import os
import shutil
import sys
import tempfile
import urlparse
import yaml

from orioluspith.packets.version import Version
from orioluspith.packets.cryptography import Cryptography, CryptographyError

class DownloadError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)

class FetchError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)

class Fetcher():
    ATTEMPTS = 3
    def __init__(self, cache):
        self.cache = cache

    def get(self, url):
        pass

    def __logerror__(self, logger, message):
        if logger:
            if logger.isEnabledFor(logging.DEBUG):
                logger.exception(message)
            else:
                logger.error(message)

class Downloader():
    def __init__(self, cache, output, fetcher, timeout=30):
        self.__logger = logging.getLogger("Downloader")
        self.__cache = os.path.abspath(cache)
        self.__output = os.path.abspath(output)

        if timeout >= 0 and timeout < 600:
            self.__timeout = timeout
        else:
            self.__timeout = 30

        self.__logger.info("Downloader init: Cache path is '%s'", self.__cache)
        self.__logger.info("Downloader init: Output path is '%s'", self.__output)

        if not os.path.exists(self.__cache):
            os.makedirs(self.__cache)

        if not os.path.exists(self.__output):
            os.makedirs(self.__output)

        self.__fetcher = fetcher(self.__cache, timeout=self.__timeout)

    def get(self, url, login = None, password = None):
        try:
            file_in_cache = self.__fetcher.get(url, login, password)

            output_file = os.path.join(self.__output, os.path.basename(file_in_cache))
            shutil.copyfile(file_in_cache, output_file)

            return output_file
        except:
            message = "Unable to download file from URL '%s'" % url
            if self.__logger.isEnabledFor(logging.DEBUG):
                self.__logger.exception(message)
            
            raise DownloadError(message)

    def clean(self):
        if (os.path.exists(self.__cache)):
            shutil.rmtree(self.__cache)

class Indexer():
    __SOURCES_LIST = "sourcelist.yaml"
    __PACKETS_LIST = "packetlist.yaml"
    __SIGNATURE_EXTENSION = ".asc"

    def __init__(self, cache, workpath, fetcher, timeout=30, verification = True, keyring = None, login = None, password = None):
        self.workpath = os.path.abspath(workpath)
        self.logger = logging.getLogger("Indexer")
        self.__download_packet_list = []
        self.__downloader = Downloader(cache, workpath, fetcher, timeout=timeout)
        self.__verification = verification
        self.__keyring = keyring
        self.__packet_list = None
        self.__login = login
        self.__password = password

    def update(self, urls_str=""):
        urls = self.get_sources(urls_str)
        files = []
        for url in urls:
            try:
                if url[-1] != "/":
                    url = url + "/"
                
                full_url = urlparse.urljoin(url, self.__PACKETS_LIST)
                filename = self.__get_resource(full_url)
                tempname = tempfile.mktemp()
                shutil.move(filename, tempname)
                files.append(tempname)
            except DownloadError, message:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.exception(message)
                else:
                    self.logger.error(message)

        self.__packet_list = PacketList()
        for file in files:
            self.__packet_list.load(file)

        self.__packet_list.save(os.path.abspath(os.path.join(self.workpath, self.__PACKETS_LIST)))

        for file in files:
            os.remove(os.path.abspath(file))
    
    def add_urls(self, urls_str):
        urls = self.get_sources(urls_str)
        filename = os.path.abspath(os.path.join(self.workpath, self.__SOURCES_LIST))
        fd = file(filename, "w")
        yaml.dump(urls, fd, default_flow_style=False)
        if fd:
            fd.close()

    def get_versions(self, packetname):
        self.__check_packet_list()
        
        return self.__packet_list.get_versions(packetname)

    def get_packet_list(self):
        self.__check_packet_list()
            
        return self.__packet_list.get_packet_list()

    def get_output_path(self):
        return self.workpath

    def download(self, packetname, version = "high"):
        self.__check_packet_list()
        
        packet = self.__packet_list.get_packet(packetname, version)

        for url in packet.urls:
            try:
                filename = self.__get_resource(url)
                break
            except DownloadError, message:
                if url == packet.urls[-1]:
                    if self.logger.isEnabledFor(logging.DEBUG):
                        self.logger.exception(message)
                    else:
                        self.logger.error(message)
                    raise

        return filename

    def read_sources(self):
        fd = None
        try:
            filename = os.path.abspath(os.path.join(self.workpath, self.__SOURCES_LIST))
            fd = file(filename, "r")
            dict = yaml.load(fd)

            if dict:
                return dict
            else:
                return []
        except IOError:
            return []
        finally:
            if fd:
                fd.close()

    def merge_urls(self, sources1, sources2):
        set1 = set(sources1)
        set2 = set(sources2)

        return sorted(list(set1 | set2))

    def get_sources(self, urls_str):
        list = urls_str.split(",")
        list = [url.strip() for url in list if url.strip() != ""]

        return self.merge_urls(self.read_sources(), list)

    def __check_packet_list(self):
        if self.__packet_list in [None]:
            try:
                self.__packet_list = PacketList()
                filename = os.path.abspath(os.path.join(self.workpath, self.__PACKETS_LIST))
                self.__packet_list.load(filename)
            except IOError, message:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.exception(message)
                else:
                    self.logger.error(message)
        
    def __get_resource(self, url):
        signature_url = url + self.__SIGNATURE_EXTENSION
        filename = self.__downloader.get(url, self.__login, self.__password)
        if self.__verification:
            signature_filename = self.__downloader.get(signature_url, self.__login, self.__password)
            verificator = Cryptography(keyring = self.__keyring)
            try:
                verificator.verify_signature(signature_filename, filename)
            except CryptographyError:
                raise DownloadError("Signature verification failed!")

        return filename

class PacketDescriptor():
    def __init__(self, name, version, urls):
        self.name = name
        self.__version = Version(version)
        self.urls = urls

    @property
    def version(self):
        return str(self.__version)

    def __eq__(self, other):
        return (other not in [None, ()] and self.name == other.name and self.__version == other.__version)

    def __cmp__(self, other):
        result = -1
        if other:
            result = cmp(self.name, other.name)
            if result == 0:
                return cmp(self.__version, other.__version)

        return result

    def __hash__(self):
        return hash(self.name + str(self.__version))

    def merge(self, other):
        if self != other:
            return None

        urls_set = set(self.urls) | set(other.urls)

        return PacketDescriptor(self.name, self.version, sorted(list(urls_set)))

class PacketList():
    def __init__(self):
        self.packets = []

    def add_packets(self, packets):
        self.__merge(packets)

    def load(self, filename):
        fd = None
        try:
            fd = file(filename, "r")
            dict = yaml.load(fd)

            if dict:
                self.__merge(map(lambda x: PacketDescriptor(x["name"], x["version"], x["urls"]), dict))
        except IOError:
            pass
            #TODO: add log here
        finally:
            if fd:
                fd.close()

    def save(self, filename):
        fd = None
        try:
            fd = file(filename, "w")
            for packet in self.packets:
                yaml.dump([{'name': packet.name, 'version': packet.version, 'urls': packet.urls}], fd, default_flow_style=False)
        except IOError:
            pass
            #TODO: add log here
        finally:
            if fd:
                fd.close()

    def get_packet_list(self):
        packet_list = []
        for packet in self.packets:
            packet_list.append(packet.name + "-" + packet.version)

        return packet_list

    def get_packet(self, packetname, version = "high"):
        if version == "high":
            versions = self.get_versions(packetname)
            if versions in [None, (), []]:
                return None
            if len(versions) > 1:
                version = self.__max_version(versions)
            else:
                version = versions[0]

        for packet in self.packets:
            if packet.name == packetname and packet.version == version:
                return packet

    def get_versions(self, packetname):
        versions = []
        if self.packets:
            for packet in self.packets:
                if packet.name == packetname:
                    versions.append(packet.version)

        return versions

    def __max_version(self, versions):
        version_obj = []
        for version in versions:
            version_obj.append(Version(version))
        sorted_version = sorted(version_obj)

        return str(sorted_version[len(sorted_version) - 1])

    def __merge(self, packets):
        if len(self.packets) == 0:
            result = packets
        else:
            result = []
            if packets and len(packets) > 0:
                for new_packet in packets:
                    for old_packet in self.packets:
                        if old_packet == new_packet:
                            if old_packet in result:
                                result.remove(old_packet)
                            result.append(old_packet.merge(new_packet))
                        else:
                            if not old_packet in result:
                                result.append(old_packet)
                            if not new_packet in result:
                                result.append(new_packet)

        self.packets = sorted(result)